/* 算法实现模板 */
import AlgorithmBase from "./AlgorithmBase";
import packages from "../../../package.json";
import MatrixViewer from "../../visual/MatrixViewer";
import ReactDOM from 'react-dom';
import React from 'react';
import Matrix from "../../common/Matrix";

import { txtToMatrix, autoNorm, classfy0, getKeyByValue } from "../../common/Common";
import Figure2D from "../../visual/Figure2D";

class KNNDatingTest extends AlgorithmBase{
  constructor(){
    super('KNNDatingTest');

    this.datasetUrl = `${packages.homepage}data/ml-in-action/ch2/datingTestSet.txt`;
    // this.testDataUrl  = `./${packages.homepage}/data/ml-in-action/ch2/datingTestSet2.txt`;

    this.dataset = '';
    // this.trainDS = {};
  }

  reset(){
    this.clear();
  }

  init(){ 
    this.clear();
    this.collect();
  }

  // 初始化收集页面
  initCollectUI(){
    const collectDiv = document.createElement('div');
    this.domElementList.collect.appendChild(collectDiv);
    this.collectDiv  = collectDiv;
  }
  // 初始化准备页面
  initPrepareUI(){
    const btnPrepare       = document.createElement('button');
    btnPrepare.innerText   = '归一化特征值';
    btnPrepare.addEventListener('click', this.prepare.bind(this));
    this.domElementList.prepare.appendChild(btnPrepare);

    const prepareDIV      = document.createElement('div');
    this.domElementList.prepare.appendChild(prepareDIV);

    this.prepareDIV = prepareDIV;
  }
  // 初始化分析页面
  initAnalysisUI(){
    const btnAnalysis       = document.createElement('button');
    btnAnalysis.innerText   = '显示特征关系图';
    btnAnalysis.addEventListener('click', this.analysis.bind(this));

    const analysisDIV      = document.createElement('div');

    this.domElementList.analysis.appendChild(btnAnalysis);
    this.domElementList.analysis.appendChild(analysisDIV);

    this.analysisDIV = analysisDIV;
  }
  // 初始化测试页面
  initTestUI(){
    const btnDoTest       = document.createElement('button');
    btnDoTest.innerText   = '开始测试';
    btnDoTest.addEventListener('click', this.doTest.bind(this));
    const ratioLabel      = document.createElement('label');
    ratioLabel.innerText  = "测试百分比";
    const ratioInput      = document.createElement('input');
    ratioInput.type       = 'number';
    ratioInput.value      = 50;
    const resultLabel     = document.createElement('label');
    const resultDiv       = document.createElement('div');

    this.domElementList.test.innerHTML = '';
    this.domElementList.test.appendChild(ratioLabel);
    this.domElementList.test.appendChild(ratioInput);
    this.domElementList.test.appendChild(btnDoTest);
    this.domElementList.test.appendChild(resultLabel);
    this.domElementList.test.appendChild(resultDiv);

    this.ratioInput   = ratioInput;
    this.resultLabel  = resultLabel;
    this.resultDiv    = resultDiv;
  }
  // 初始化使用页面
  initUsageUI(){
    const labelFlyMiles   = document.createElement('label');
    const inputFlyMiles   = document.createElement('input');
    const labelGameTime   = document.createElement('label');
    const inputGameTime   = document.createElement('input');
    const labelIceGallons = document.createElement('label');
    const inputIceGallons = document.createElement('input');

    inputGameTime.type = 'number';
    inputFlyMiles.type = 'number';
    inputIceGallons.type = 'number';

    inputGameTime.value = 0;
    inputFlyMiles.value = 0;
    inputIceGallons.value = 0;

    labelGameTime.innerText = '游戏时间占比';
    labelFlyMiles.innerText = '飞行里程数';
    labelIceGallons.innerText = '冰淇淋消耗';

    const btnUsage = document.createElement('button');
    btnUsage.innerText = '计算';
    btnUsage.addEventListener('click', this.usage.bind(this));
    const usageResultDIV   = document.createElement('div');

    this.domElementList.usage.appendChild(labelFlyMiles);
    this.domElementList.usage.appendChild(inputFlyMiles);
    this.domElementList.usage.appendChild(labelGameTime);
    this.domElementList.usage.appendChild(inputGameTime);
    this.domElementList.usage.appendChild(labelIceGallons);
    this.domElementList.usage.appendChild(inputIceGallons);
    this.domElementList.usage.appendChild(btnUsage);
    this.domElementList.usage.appendChild(usageResultDIV);

    this.inputGameTime    = inputGameTime;
    this.inputFlyMiles    = inputFlyMiles;
    this.inputIceGallons  = inputIceGallons;
    this.usageResultDIV   = usageResultDIV;
  }

  // 载入文本数据
  loadTrainDS(url, args){
    this.collect(url);
  }

  // 收集数据
  collect(url){
    if(!url) url = this.datasetUrl;
    fetch(url).then((response)=>{
      return response.text();
    }).then((txt)=>{
      // this.load(txt);  
      this.dataset = txtToMatrix(txt);
      ReactDOM.unmountComponentAtNode(this.collectDiv);
      ReactDOM.render(
        <React.StrictMode>
          <MatrixViewer 
            matrix={txtToMatrix(txt)}
          />
        </React.StrictMode>,
        this.collectDiv
      )    
    })
  }
  // 准备数据
  prepare(){
    let trainDS = {};
    const labelsDict    = {'largeDoses':3, 'smallDoses':2, 'didntLike':1};
    trainDS = this.dealwithDataset(this.dataset, labelsDict);
    if(trainDS){
      this.trainDS = trainDS;
      // this.prepareDIV.innerHTML = '';
      ReactDOM.unmountComponentAtNode(this.prepareDIV);
      ReactDOM.render(
        <React.StrictMode>
          <MatrixViewer 
            matrix={Matrix.columnStack(trainDS.Mat, trainDS.Labels)}
          />
        </React.StrictMode>,
        this.prepareDIV
      );
    }
  }

  // 处理文本数据
  // 分离数据和标签
  // 归一化特征值
  dealwithDataset(dataset, labelsDict){
    if(!dataset){
      return undefined;
    }
    // 将原始数据读取成数组和标签
    let normalized    = {};
    normalized.Mat    = new Matrix.Matrix();
    normalized.Labels = new Matrix.Matrix();
    for(let i=0, il=dataset.length; i < il; i++){
      normalized.Mat.push(dataset[i].slice(0, -1));
      if(!labelsDict){
        normalized.Labels.push(dataset[i][dataset[i].length - 1]);
      }
      else{
        normalized.Labels.push(labelsDict[dataset[i][dataset[i].length - 1]]);
      }
    }
    // 归一化数据
    normalized.Mat = autoNorm(normalized.Mat);
    return normalized;
  }

  // 分析数据
  analysis(){
    if(!this.trainDS){
      this.analysisDIV.innerText=`训练数据未准备好...`;
      return;
    } 
    this.analysisDIV.innerHTML = '';
    this.showMatInFigure(this.trainDS.Mat, this.trainDS.Labels, this.analysisDIV);
  }

  // 载入测试数据
  // 初始化测试页面
  test(){
    // this.testDS  = this.trainDS;
    this.doTest();
  }

  doTest(){
    const testDS  = this.trainDS;
    if(!testDS){
      this.resultLabel.innerText=`训练数据未准备好...`;
      return;
    } 

    let ratio     = parseFloat(this.ratioInput.value);

    if(ratio < 10 || ratio > 90){
      ratio = 50;
      this.ratioInput.value = ratio;
    }

    const m           = Math.floor(ratio * testDS.Mat.length / 100);
    const testMat     = testDS.Mat.slice(0, m);
    const testLabels  = testDS.Labels.slice(0, m);
    const trainMat    = testDS.Mat.slice(m);
    const trainLabels = testDS.Labels.slice(m);
    let errResult     = new Matrix.Matrix();

    let errCount = 0;
    for(let i=0; i < m; i++){
      const classifierResult = classfy0(testMat[i], trainMat, trainLabels, 3);

      if( parseInt(testLabels[i]) !== parseInt(classifierResult)){
        // console.log(`误分类点:${i},实际值${testLabels[i]},预测值${classifierResult}`);
        errResult.push([i, ...testMat[i], testLabels[i], classifierResult]);
        errCount ++;
      }
    }

    ReactDOM.unmountComponentAtNode(this.resultDiv);
    ReactDOM.render(
      <React.StrictMode>
        <MatrixViewer 
          matrix={errResult}
        />
      </React.StrictMode>,
      this.resultDiv
    )

    this.resultLabel.innerText=`错误率:${(errCount / m).toFixed(6)}`;
  }

  // 使用算法
  usage(){
    this.usageResultDIV.innerText = '';

    if(!this.dataset){
      this.usageResultDIV.innerText = `未加载样本数据...`;
      return;
    }

    const gameTime    = this.inputGameTime.value;
    const flyMiles    = this.inputFlyMiles.value;
    const iceGallons  = this.inputIceGallons.value;
    let dataset = [];
    dataset.push([flyMiles, gameTime, iceGallons, 'didntLike']);
    dataset.push(...this.dataset);
    // 归一化特征值
    const labelsDict    = {'largeDoses':3, 'smallDoses':2, 'didntLike':1};
    const normalizedDataset = this.dealwithDataset(dataset,  labelsDict);
    // console.log(normalizedDataset);
    const classifierResult = classfy0(normalizedDataset.Mat[0], normalizedDataset.Mat.slice(1), normalizedDataset.Labels.slice(1), 3);
    this.usageResultDIV.innerText = `分类结果：${classifierResult}-${getKeyByValue(labelsDict, parseInt(classifierResult))}`;
  }

  // 在图中显示矩阵
  showMatInFigure(mat, labels, dom){
    if(!mat || !labels || !dom) return;

    const labelsDict    = {3:'largeDoses', 2:'smallDoses', 1:'didntLike', 0:'unused'};
    const colorDict     = {0:"#000000", 1:"#ff0000", 2:"#00ff00", 3:"#0000ff"};
    const axisLabelDict = {0:"游戏时间占比",1:"飞行里程数",2:"冰淇淋消耗"};
    const fig = new Figure2D(800, 600, [0, 1], [0, 1]);
    fig.xAxisOrigin = -0.5;
    fig.yAxisOrigin = -0.5;

    // 添加控制控件
    const xLabel = document.createElement('label');
    const yLabel = document.createElement('label');
    xLabel.innerText = 'X轴:';
    yLabel.innerText = 'Y轴:';
    const xSelect = document.createElement('select');
    const ySelect = document.createElement('select');
    xSelect.addEventListener('change', this.updateFigure.bind(this));
    ySelect.addEventListener('change', this.updateFigure.bind(this));
    for(let i=0;i<mat.cols();i++){
      const optx = document.createElement('option');
      const opty = document.createElement('option');
      optx.text = axisLabelDict[i];
      opty.text = axisLabelDict[i];
      xSelect.appendChild(optx);
      ySelect.appendChild(opty);
    }
    xSelect.selectedIndex = 0;
    fig.xAxisLabel = axisLabelDict[0];
    ySelect.selectedIndex = 1;
    fig.yAxisLabel = axisLabelDict[1];

    for(let i = 0, il=mat.length; i < il ; i++){
      fig.addPoint(parseFloat(mat[i][1]), parseFloat(mat[i][2]), 3, 0, colorDict[labels[i]]);
    }

    // 添加选项
    dom.appendChild(xLabel);
    dom.appendChild(xSelect);
    dom.appendChild(yLabel);
    dom.appendChild(ySelect);
    // 添加颜色指示
    for(let key in colorDict){
      const colorLabel = document.createElement('label');
      colorLabel.innerText = labelsDict[key];
      const colorInput = document.createElement('input');
      colorInput.type  = 'color';
      colorInput.id    = key;
      colorInput.value = colorDict[key];
      colorInput.addEventListener('change', this.updateColorDict.bind(this));
      dom.appendChild(colorLabel);
      dom.appendChild(colorInput);
    }
    dom.appendChild(fig.domElement);
    fig.update();

    this.xSelect = xSelect;
    this.ySelect = ySelect;
    this.fig     = fig;
    this.mat     = mat;
    this.labels  = labels;
    this.colorDict = colorDict;
    this.axisLabelDict = axisLabelDict;
  }

  // 更新图
  updateFigure(){
    const axisLabelDict = this.axisLabelDict;
    const colorDict = this.colorDict;
    const fig = this.fig;
    const mat = this.mat;
    const labels = this.labels;
    let x = parseInt( this.xSelect.selectedIndex );
    let y = parseInt( this.ySelect.selectedIndex );
    if(x === y){
      y = (y + 1) % mat.cols();
      this.ySelect.selectedIndex = y;
    }
    fig.xAxisLabel = axisLabelDict[x];
    fig.yAxisLabel = axisLabelDict[y];

    fig.clearAll();
    for(let i = 0, il=mat.length; i < il ; i++){
      fig.addPoint(parseFloat(mat[i][x]), parseFloat(mat[i][y]), 3, 0, colorDict[labels[i]]);
    }
    fig.update();
  }
  // 更新图例颜色
  updateColorDict(event){
    const id = event.target.id;
    this.colorDict[id] = event.target.value;
    this.updateFigure();
  }
}


export default KNNDatingTest;